feat!: copy middleware context inputs to prevent accidental mutation#2742
Conversation
Middleware now receives isolated copies of agent state rather than live references. Messages are deep-cloned via Message.clone(), systemPrompt via cloneSystemPrompt(), toolSpecs/toolChoice via structuredClone(), and invocationState via shallow copy. modelState is excluded from the context entirely and snapshotted/written-back outside the middleware chain so middleware cannot affect it at any point.
The project's eslint config uses an explicit globals allowlist that doesn't include structuredClone. Use the existing deepCopy utility instead.
|
Assessment: Comment Solid, well-tested defensive-copy change with a clear design rationale documented in the middleware README and PR description. The snapshot/writeback approach for Review Categories
Nice work isolating middleware from agent internals — the functional-style test cases passing modified contexts to |
ExecuteToolStage: deep-copy toolUse so middleware cannot mutate the model's parsed tool input in the assistant message. AgentStreamStage: shallow-copy array args so middleware cannot push/splice the caller's original array. invocationState is intentionally left as a shared reference across all stages — tools and hooks write to it and those mutations must appear on AgentResult.
This comment was marked as outdated.
This comment was marked as outdated.
|
Re-review (round 5) — Assessment: Approve ✅ Reviewed Verified locally on this HEAD:
All blocking items are resolved. The One optional, non-blocking follow-upThe "message content blocks are deep copied" test ( |
The context contract for AgentStreamStage (copy vs. reference semantics for args/options) is not yet finalized. Rather than ship an inconsistent surface alongside the other stages' clear guarantees, keep it internal until we decide on the right approach.
|
Re-review (round 6) — Assessment: Approve ✅ Reviewed
Verified locally on this HEAD:
Two minor, non-blocking notes: Doc nit + open thread to reconcile
Still approving — the remaining items are documentation/discussion cleanup, not code defects. |
Description
Middleware previously received live references to the agent's internal state. A middleware that accidentally mutated the messages array, a toolSpec, or modelState would corrupt agent state for subsequent turns.
This PR makes middleware context inputs defensive copies across the public stages:
InvokeModelStage —
messages,systemPrompt,toolSpecs, andtoolChoiceare all deep-copied.modelStateis removed from the context entirely (snapshotted before the chain, written back after).invocationStateremains shared by reference since hooks and tools write to it during streaming.ExecuteToolStage —
toolUseis deep-copied to prevent mutation of the parsed tool input in the assistant message.invocationStateremains shared by reference.AgentStreamStage — Removed from the public API. The context contract (copy vs. reference for
args/options) isn't finalized, and shipping it with different guarantees than the other stages would be confusing. It continues to work internally.Design rationale is documented in
strands-ts/src/middleware/README.md.Related Issues
Type of Change
Breaking change
Breaking Changes
modelStateremoved fromInvokeModelContextAgentStreamStage,AgentStreamContext,AgentStreamResultremoved from public exportsBoth are acceptable — middleware just shipped and has no external consumers yet.
Testing
23 tests in
copy-on-input.test.tscovering isolation guarantees for all copied fields, shared-reference behavior forinvocationState, modelState writeback semantics, and functional-style context passing.hatch run prepareChecklist
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.